package br.pucrio.inf.lac.konoha.hyuga.others.csv;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.log4j.Logger;

import br.pucrio.inf.lac.konoha.hyuga.core.output.CSVEntry;

public class ClusteringMeasurementTool {

    /** LOGGER. */
    private static Logger LOGGER = Logger.getLogger(ClusteringMeasurementTool.class.getCanonicalName());

    private static final ClusteringMeasurementTool instance = new ClusteringMeasurementTool(); 
    
    private ClusteringMeasurementTool() {}
    
    public static ClusteringMeasurementTool getInstance() {
        return instance;
    }
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    // Util
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    public CSVEntry getEntryWithHigherRandIndex(CSVEntry dg2cepEntry, List<CSVEntry> dbscanClustersInThisSecond) {
        CSVEntry dbscanHigherRandIndexEntry = null;
        double higherRandIndexValue = 0;
        double randIndexValue = 0;
        
        if (dbscanClustersInThisSecond != null) {
            for (CSVEntry dbscanEntry : dbscanClustersInThisSecond) {
                randIndexValue = recall(dg2cepEntry, dbscanEntry);
                if (higherRandIndexValue <= randIndexValue) {
                    higherRandIndexValue = randIndexValue;
                    dbscanHigherRandIndexEntry = dbscanEntry;
                }
            }
        }
        
        return dbscanHigherRandIndexEntry;
    }
    
    public List<CSVEntry> getEntriesWithHigherRandIndex(CSVEntry dg2cepEntry, List<CSVEntry> dbscanClustersInThisSecond, 
                                                        double threshold) {
        List<CSVEntry> list = new ArrayList<>();
        double higherRandIndexValue = 0;
        double precision = 0;
        
        if (dbscanClustersInThisSecond != null) {
            for (CSVEntry dbscanEntry : dbscanClustersInThisSecond) {
                precision = recall(dg2cepEntry, dbscanEntry);
                if (precision >= threshold) {
                    list.add(dbscanEntry);
                }
            }
        }
        
        return list;
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    // Precision
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    public double precision(CSVEntry dg2cepEntry, CSVEntry dbscanEntry) {
        if (dbscanEntry == null) { return 0; }
        
        Set<String> dg2cepSet = dg2cepEntry.getContent();
        Set<String> dbscanSet = dbscanEntry.getContent();
        
        Set<String> truePositive = new HashSet<String>(dg2cepSet);
        truePositive.retainAll(dbscanSet);
      
        Set<String> falsePositive = new HashSet<String>(dg2cepSet);
        falsePositive.removeAll(dbscanSet);

        double TP = combination(truePositive.size(), 2);
        double FP = combination(falsePositive.size(), 2);

        return TP / (TP + FP);
    }
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    // Recall
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    public double recall(CSVEntry dg2cepEntry, CSVEntry dbscanEntry) {
        if (dbscanEntry == null) { return 0; }
        
        Set<String> dg2cepSet = dg2cepEntry.getContent();
        Set<String> dbscanSet = dbscanEntry.getContent();
        
        Set<String> truePositive = new HashSet<String>(dg2cepSet);
        truePositive.retainAll(dbscanSet);
      
        Set<String> falseNegative = new HashSet<String>(dbscanSet);
        falseNegative.removeAll(dg2cepSet);

        double TP = combination(truePositive.size(), 2);
        double FN = combination(falseNegative.size(), 2);

        return TP / (TP + FN);
    }
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    // F-Measure
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    public double fMeasure(CSVEntry dg2cepEntry, CSVEntry dbscanEntry) {
        if (dbscanEntry == null) { return 0; }
        
        double precision = precision(dg2cepEntry, dbscanEntry);
        double recall    = recall(dg2cepEntry, dbscanEntry);
        
        return (2 * (precision * recall)) / (precision + recall);
    }
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    // Jaccard Distance
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    public double jaccardDistance(CSVEntry dg2cepEntry, CSVEntry dbscanEntry) {
        if (dbscanEntry == null) { return 0; }
        
        Set<String> dg2cepSet = dg2cepEntry.getContent();
        Set<String> dbscanSet = dbscanEntry.getContent();
        
        Set<String> intersection = new HashSet<String>(dg2cepSet);
        intersection.retainAll(new HashSet<String>(dbscanSet));
      
        Set<String> union = new HashSet<String>(dg2cepSet);
        union.addAll(new HashSet<String>(dbscanSet));

        double IN = intersection.size();
        double UN = union.size();

        return (double) IN / UN;
    }
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    // Set to Set Rand Index
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    public double randIndex(CSVEntry dg2cepEntry, CSVEntry dbscanEntry) {
        if (dbscanEntry == null) { return 0; }
        
        Set<String> dg2cepSet = dg2cepEntry.getContent();
        Set<String> dbscanSet = dbscanEntry.getContent();
        
        Set<String> truePositive = new HashSet<String>(dg2cepSet);
        truePositive.retainAll(dbscanSet);
      
        Set<String> falsePositive = new HashSet<String>(dg2cepSet);
        falsePositive.removeAll(dbscanSet);

        Set<String> falseNegative = new HashSet<String>(dbscanSet);
        falseNegative.removeAll(dg2cepSet);
      
        double TP = combination(truePositive.size(), 2);
        double FP = combination(falsePositive.size(), 2);
        double FN = combination(falseNegative.size(), 2);

        return TP / (TP + FP + FN);
    }
    
    public double realRandIndex(CSVEntry dg2cepEntry, CSVEntry dbscanEntry) {
        return adjustedRandIndex(Arrays.asList(dg2cepEntry), Arrays.asList(dbscanEntry), false);
    }
    
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    // Adjusted Rand Index
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    public double adjustedRandIndex(List<CSVEntry> dg2cepEntries, List<CSVEntry> dbscanEntries) {
        return adjustedRandIndex(dg2cepEntries, dbscanEntries, true);
    }
    
    public double adjustedRandIndex(List<CSVEntry> dg2cepEntries, List<CSVEntry> dbscanEntries, boolean adjusted) {
        // Null Checking
        if (dbscanEntries == null) { return 0; }
        if (dg2cepEntries == null) { return 0; }
        if (dg2cepEntries.size() == 0) { return 0; }
        if (dbscanEntries.size() == 0) { return 0; }

        HashSet<String> scopeUniverseSet = new HashSet<String>();
        for (CSVEntry dbscanEntry : dbscanEntries) {
            scopeUniverseSet.addAll(dbscanEntry.getContent());
        }
        
        for (CSVEntry dg2cepEntry : dg2cepEntries) {
            scopeUniverseSet.addAll(dg2cepEntry.getContent());
        }
        
        // DBSCAN and DG2CEP Universe Set
        Set<String> dbscanRemainderSet = new HashSet<String>(scopeUniverseSet);
        Set<String> dg2cepRemainderSet = new HashSet<String>(scopeUniverseSet);
        
        for (CSVEntry dbscanEntry : dbscanEntries) {
            dbscanRemainderSet.removeAll(dbscanEntry.getContent());
        }
        
        for (CSVEntry dg2cepEntry : dg2cepEntries) {
            dg2cepRemainderSet.removeAll(dg2cepEntry.getContent());
        }
        
        int lasti  = dbscanEntries.size();
        int lastj  = dg2cepEntries.size();
        int n[][]  = new int[lasti + 2][lastj + 2];
        int rowsum = 0;
        int colsum = 0;

        System.out.println("----------------------------------------------------------------");
        for (int i = 0; i < lasti; i++) {
            CSVEntry dbscanEntry = dbscanEntries.get(i);
            rowsum = 0;
            
            System.out.print("| ");
            for (int j = 0; j < lastj; j++) {
                CSVEntry dg2cepEntry = dg2cepEntries.get(j);
                n[i][j] = intersection(dbscanEntry.getContent(), dg2cepEntry.getContent());
                rowsum += n[i][j];
                
                System.out.printf("%5d", n[i][j]);
            }
            
            // DBSCAN Remainder Column
            n[i][lastj] = intersection(dbscanEntry.getContent(), dg2cepRemainderSet);
            rowsum+= n[i][lastj];
            System.out.printf("%5d   = ", n[i][lastj]);
            
            n[i][lastj + 1] = rowsum;
            System.out.printf("%5d |\n", n[i][lastj+1]);
        }

        // DG2CEP Remainder Row
        rowsum = 0;
        System.out.print("| ");
        for (int j = 0; j < lastj; j++) {
            CSVEntry dg2cepEntry = dg2cepEntries.get(j);
            n[lasti][j] = intersection(dbscanRemainderSet, dg2cepEntry.getContent());
            rowsum += n[lasti][j];
            
            System.out.printf("%5d", n[lasti][j]);
        }
        n[lasti][lastj] = intersection(dbscanRemainderSet, dg2cepRemainderSet);
        rowsum += n[lasti][lastj];
        System.out.printf("%5d   = ", n[lasti][lastj]);
        
        n[lasti][lastj + 1] = rowsum;
        System.out.printf("%5d |\n", n[lasti][lastj + 1]);
        

        // Last Row (column totals)
        System.out.println("================================================================");
        System.out.print("| ");
        int total = 0;
        for (int j = 0; j < lastj + 2; j++) {
            colsum = 0;
            for (int i = 0; i < lasti + 1; i++) {
                colsum += n[i][j];
            }
            
            n[lasti + 1][j] = colsum;
            
            if (j == lastj + 1) {
                System.out.printf("%5d |\n",  n[lasti + 1][j]);    
            } else if (j == lastj) {
                total += colsum;
                System.out.printf("%5d   = ", n[lasti + 1][j]);
            } else {
                total += colsum;
                System.out.printf("%5d",      n[lasti + 1][j]);
            }
        }
        System.out.println("----------------------------------------------------------------");
        System.out.println("\n");
        
        double a = 0;
        for (int i = 0; i < lasti + 1; i++) {
            for (int j = 0; j < lastj + 1; j++) {
                a += combination(n[i][j], 2);
            }
        }
        
        // Expected Value (according to adj rand index)
        double b = 0;
        double c = 0;
        
        for (int i = 0; i < lasti + 1; i++) {
            // System.out.print(n[i][lastj + 1]+",");
            b = b + combination(n[i][lastj + 1], 2);
        }
        System.out.println("\n");
        
        for (int j = 0; j < lastj + 1; j++) {
            // System.out.print(n[lasti + 1][j]+",");
            c = c + combination(n[lasti + 1][j], 2);
        }
        
        b = b - a;
        c = c - a;
        
        double d = (combination(total, 2)) - a - b - c;
        
        double x = ((a+b)*(a+c)) + ((c+d)*(b+d));
        double p1 = ((combination(total, 2)) * (a+d)) - x;
        double p2 = ((combination(total, 2)) * (combination(total, 2))) - x;
        
        double randIndex = (a + d) / (a + b + c + d);
        double adjustedRandIndex = p1 / p2;

        if (adjusted) {
            return adjustedRandIndex;
        } else {
            return randIndex;
        }
    }

    private int intersection(Set<String> firstSet, Set<String> secondSet) {
        Set<String> intersection = new HashSet<String>(firstSet);
        intersection.retainAll(secondSet);
        
        return intersection.size();
    }

    public Long combination(int n, int k) {
        if      (n <= 0) return 0L;
        else if (k <= 0) return 0L;
        else if (n < k)  return 0L;
        else if (n == k) return 1L;
        else             return (long) ((n * (n-1))/2);
    }
}
